2026_french_79_between_the_buttons.py

#

SPDX-FileCopyrightText: 2026 Béatrice Versickas & Mark Akoury SPDX-FileCopyrightText: 2026 AlICe laboratory https://alicelab.be

SPDX-License-Identifier: GPL-3.0-or-later

import math
import random

import bmesh
import bpy
#

------------------------------------------------------------------------------------CLEAN

bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete(use_global=False)
#

------------------------------------------------------------------------------------MELODY and CHORDS

#

creation de vaque orgnaique

def create_organic_wave(
    nom_objet, z_location, amplitude, freq, time_offset, octree_depth, scale, threshold
):
    taille_grille = 10
    subdivisions = 100
    organic_blend = 1.0
#

supprimer l’ancienne grille si elle existe

    if nom_objet in bpy.data.objects:
        bpy.data.objects.remove(bpy.data.objects[nom_objet], do_unlink=True)
#

créer une nouvelle grille

    bpy.ops.mesh.primitive_grid_add(
        size=taille_grille,
        x_subdivisions=subdivisions,
        y_subdivisions=subdivisions,
        location=(0, 0, z_location),
    )
#

répéter la grille

    obj = bpy.context.active_object
    obj.name = nom_objet
#

déformation

    mesh = obj.data
    bm = bmesh.new()
    bm.from_mesh(mesh)
#

modifier les points de la grille

    for v in bm.verts:
        x = v.co.x
        y = v.co.y
#

vagues

        wave1 = math.sin((x + y) * freq + time_offset)
        wave2 = math.cos((x - y * 1.3) * freq * 0.8 - time_offset)
#

distance au centre pour avoir une sorte d’onde circulaire

        distance = math.sqrt(x**2 + y**2)
        radial = math.sin(distance * freq * 1.5 - time_offset * 1.2)

        v.co.z = amplitude * (wave1 + wave2 + (organic_blend * radial)) / 3.0

    bm.to_mesh(mesh)
    bm.free()
    mesh.update()
#

créer les sortes de pixels avec le modifiers remesh en blocks

    remesh_mod = obj.modifiers.new(name="Voxel_Blocks", type="REMESH")
    remesh_mod.mode = "BLOCKS"
    remesh_mod.octree_depth = octree_depth  # niveau de détail
    remesh_mod.scale = scale  # taille globale du remesh
    remesh_mod.threshold = threshold  # définit un seuil

    bpy.ops.object.shade_flat()  # évite de faire un effet lisse car on veut des voxels
#

GENERER LES VAGUES

#

Choix d’une fréquence aléatoire qui sera partagée par les deux vagues

freq_partagee = random.uniform(0.25, 0.4)  # pour qu'elles s'entremelent
offset_base = random.uniform(0, math.pi * 2)  # pour éviter que ce soit symétrique
#

vague 1 = chords

create_organic_wave(
    nom_objet="Wave_1",
    z_location=4.5,
    amplitude=2.5,  # force des vagues
    freq=freq_partagee,  # meme fréquence que la vague 2
    time_offset=offset_base,
    octree_depth=5,  # faible détails pour avoir de grands voxels
    scale=0.9,  # taille globale
    threshold=1.0,
)
#

vague 2 = melody

create_organic_wave(
    nom_objet="Wave_2",
    z_location=6,
    amplitude=4.5,
    freq=freq_partagee,
    time_offset=offset_base + 1.0,  # décalage pour avoir un mouvement diff
    octree_depth=8,
    scale=0.9,
    threshold=1.0,
)
#

------------------------------------------------------------------------------------KICKS

#

collection

col_name = "GENERATED_KICK"
#

si le collection existe on la recupère sinon on l’a créer

if col_name in bpy.data.collections:
    target_col = bpy.data.collections[col_name]
else:
    target_col = bpy.data.collections.new(col_name)
    bpy.context.scene.collection.children.link(target_col)
#

choix aléatoire des 4 différents kicks

style = random.randint(1, 4)
#

grille

max_height = 10.0
grid_size = 10.0
offset = grid_size / 2  # recentre la grille sur 0,0
#

chaque kick est différent

if style == 1:
    step = 3
    min_colonnes = 4
elif style == 2:
    step = 3
    min_colonnes = 6
elif style == 3:
    step = 3
    min_colonnes = 4
else:
    step = 1
    min_colonnes = 8
#

creation grille

n = int(grid_size // step)  # nombre de cases sur la grille
positions = []
#

on génère tout les points possibles sur la grille

for i in range(n):
    for j in range(n):
#

générer tout les points possibles sur la grille

        x = (i * step + step / 2) - offset
        y = (j * step + step / 2) - offset
        positions.append((x, y))
#

selection random des points

nombre_colonnes = random.randint(min_colonnes, len(positions))
positions_choisies = random.sample(positions, nombre_colonnes)
#

creer les cubes

for x, y in positions_choisies:
    h = random.uniform(1.0, max_height)  # hauteur aléatoire

    bpy.ops.mesh.primitive_cube_add(location=(x, y, h / 2))  # le cube
    obj = bpy.context.object
#

chaque kick est diff

    if style == 1:
        obj.scale = (0.35, 0.35, h / 2)
    elif style == 2:
        if random.random() < 0.5:
            obj.scale = (0.35, 0.35, h / 2)
        else:
            obj.scale = (0.15, 0.15, h / 2)
    elif style == 3 or style == 4:
        obj.scale = (0.15, 0.15, h / 2)
#

objet dans la collection

    if obj.name not in target_col.objects:
        target_col.objects.link(obj)
#

retire des autres collections

    for col in obj.users_collection:
        if col != target_col:
            col.objects.unlink(obj)
#

------------------------------------------------------------------------------------BASS

selected_bass = random.randint(1, 4)  # selection random des 4 basses diff que l'on a
#

grille

if selected_bass == 1:
#

grille regulière

    step = 1.0
    zone_size = 10
#

décalage pour centrer la grille

    offset_x = -5
    offset_y = -5
#

hauteurs

    max_height = 1.5
    min_height = 0.8
#

taille

    size = 1.0
#

------------------------------------------------------------------------------------basse1

#

collection basse1

    if "basse1" in bpy.data.collections:
        basse1 = bpy.data.collections["basse1"]
    else:
        basse1 = bpy.data.collections.new("basse1")
        bpy.context.scene.collection.children.link(basse1)
#

Nombre de cubes par côté (10 / 1.0 = 10)

    n = int(zone_size / step)

    for i in range(n):
        for j in range(n):
#

on calcule le coin de la cellule

            corner_x = i * step
            corner_y = j * step
#

hauteur aléatoire

            h = random.uniform(min_height, max_height)
#
  1. On trouve le centre PARFAIT
            center_x = corner_x + (size / 2)
            center_y = corner_y + (size / 2)
#

centre exacte

            x = center_x + offset_x
            y = center_y + offset_y
#

cube

            bpy.ops.mesh.primitive_cube_add(location=(x, y, h / 2))
            obj = bpy.context.object
#

mise à l’échelle

            obj.scale = (size / 2, size / 2, h / 2)
#

mettre dans collection basse1

            for col in obj.users_collection:
                col.objects.unlink(obj)

            basse1.objects.link(obj)
#

------------------------------------------------------------------------------------basse2

elif selected_bass == 2:
    step = 0.5
    zone_size = 10

    offset_x = -5
    offset_y = -5
#

petits et gros blocs avec des hauteurs diffs

    max_small = 1.5
    min_small = 0.8

    max_big = 3.5
    min_big = 2.0
#

tailles

    size_small = 0.5
    size_big = 1.5
#

collection basse2

    if "basse2" in bpy.data.collections:
        basse2 = bpy.data.collections["basse2"]
    else:
        basse2 = bpy.data.collections.new("basse2")
        bpy.context.scene.collection.children.link(basse2)
#

probabilité grosse colonne

    prob_big = 0.04
#

10 / 0.5 = 20 on boucle exactement 20 fois (de 0 à 19)

    n = int(zone_size / step)

    for i in range(n):
        for j in range(n):
#

calcule le coin de la cellule

            corner_x = i * step
            corner_y = j * step
#

choix aléatoire (gros bloc ou petits)

            is_big = random.random() < prob_big
#

évite que les gros blocs dépassent de la grille

            if is_big and (
                (corner_x + size_big > zone_size) or (corner_y + size_big > zone_size)
            ):
                is_big = False
#

taille + hauteur selon type

            if is_big:
                size = size_big
                h = random.uniform(min_big, max_big)
            else:
                size = size_small
                h = random.uniform(min_small, max_small)

            center_x = corner_x + (size / 2)
            center_y = corner_y + (size / 2)

            x = center_x + offset_x
            y = center_y + offset_y

            bpy.ops.mesh.primitive_cube_add(location=(x, y, h / 2))
            obj = bpy.context.object

            obj.scale = (size / 2, size / 2, h / 2)

            for col in obj.users_collection:
                col.objects.unlink(obj)

            basse2.objects.link(obj)
#

------------------------------------------------------------------------------------basse3

elif selected_bass == 3:
    step = 0.5
    zone_size = 10

    offset_x = -5
    offset_y = -5
#

hauteurs

    max_height = 1.5
    min_height = 0.8
#

taille

    size = 0.5
#

collection basse3

    if "basse3" in bpy.data.collections:
        basse3 = bpy.data.collections["basse3"]
    else:
        basse3 = bpy.data.collections.new("basse3")
        bpy.context.scene.collection.children.link(basse3)
#

nombre de cubes par côté (10 / 0.5 = 20)

    n = int(zone_size / step)

    for i in range(n):
        for j in range(n):
            corner_x = i * step
            corner_y = j * step

            h = random.uniform(min_height, max_height)

            center_x = corner_x + (size / 2)
            center_y = corner_y + (size / 2)

            x = center_x + offset_x
            y = center_y + offset_y

            bpy.ops.mesh.primitive_cube_add(location=(x, y, h / 2))
            obj = bpy.context.object

            obj.scale = (size / 2, size / 2, h / 2)

            for col in obj.users_collection:
                col.objects.unlink(obj)

            basse3.objects.link(obj)
#

------------------------------------------------------------------------------------basse4

elif selected_bass == 4:
#

Paramètres

    step = 0.5
    zone_size = 10

    offset_x = -5
    offset_y = -5
#

hauteurs

    max_small = 3.5
    min_small = 1.5

    max_big = 2
    min_big = 0.8
#

tailles

    size_small = 0.5
    size_big = 1.5
#

collection basse4

    if "basse4" in bpy.data.collections:
        basse4 = bpy.data.collections["basse4"]
    else:
        basse4 = bpy.data.collections.new("basse4")
        bpy.context.scene.collection.children.link(basse4)
#

probabilité grosse colonne

    prob_big = 0.1
#

10 / 0.5 = 20. On boucle exactement 20 fois (de 0 à 19)

    n = int(zone_size / step)
#

pour ne pas que les cubes se mettent dans d’autres cubes (savoir les cases deja utilisées)

    occupied = set()
#

taille d’un gros bloc en case (1.5 / 0.5 = 3)

    span = int(round(size_big / step))

    for i in range(n):
        for j in range(n):
#

si case déja utilisée

            if (i, j) in occupied:
                continue

            corner_x = i * step
            corner_y = j * step

            is_big = random.random() < prob_big
#

verifier limites et coliisions

            if is_big:
#

vérifie s’il déborde des bords du 10x10

                if (corner_x + size_big > zone_size) or (
                    corner_y + size_big > zone_size
                ):
                    is_big = False
                else:
#

vérifie si l’espace 3x3 requis est totalement vide

                    for dx in range(span):
                        for dy in range(span):
                            if (i + dx, j + dy) in occupied:
                                is_big = False
                                break
                        if not is_big:
                            break
#

taille + rappel

            if is_big:
                size = size_big
                h = random.uniform(min_big, max_big)
#

marque toute la zone 3x3 comme occupée

                for dx in range(span):
                    for dy in range(span):
                        occupied.add((i + dx, j + dy))
            else:
                size = size_small
                h = random.uniform(min_small, max_small)
#

marque juste cette petite case comme occupée

                occupied.add((i, j))
#

on trouve le centre PARFAIT en ajoutant la moitié de la taille au coin

            center_x = corner_x + (size / 2)
            center_y = corner_y + (size / 2)
#

offset pour la position finale

            x = center_x + offset_x
            y = center_y + offset_y

            bpy.ops.mesh.primitive_cube_add(location=(x, y, h / 2))
            obj = bpy.context.object

            obj.scale = (size / 2, size / 2, h / 2)

            for col in obj.users_collection:
                col.objects.unlink(obj)

            basse4.objects.link(obj)